class Vector2 {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }

  add(v) {
    this.x += v.x;
    this.y += v.y;
    return this;
  }

  multiplyScalar(s) {
    this.x *= s;
    this.y *= s;
    return this;
  }

  clone() {
    return new Vector2(this.x, this.y);
  }}


class Time {
  constructor() {
    const now = Time.now();

    this.delta = 0;
    this.elapsed = 0;
    this.start = now;
    this.previous = now;
  }

  update() {
    const now = Time.now();

    this.delta = now - this.previous;
    this.elapsed = now - this.start;
    this.previous = now;
  }

  static now() {
    return Date.now() / 1000;
  }}


class Particle {
  constructor(position, velocity = new Vector2(), color = 'white', radius = 1, lifetime = 1, mass = 1) {
    this.position = position;
    this.velocity = velocity;
    this.color = color;
    this.radius = radius;
    this.lifetime = lifetime;
    this.mass = mass;

    this.isInCanvas = true;
    this.createdOn = Time.now();
  }

  update(time) {
    if (!this.getRemainingLifetime()) {
      return;
    }

    this.velocity.add(Particle.GRAVITATION.clone().multiplyScalar(this.mass));
    this.position.add(this.velocity.clone().multiplyScalar(time.delta));
  }

  render(canvas, context) {
    const remainingLifetime = this.getRemainingLifetime();

    if (!remainingLifetime) return;

    const radius = this.radius * remainingLifetime;

    context.globalAlpha = remainingLifetime;
    context.globalCompositeOperation = 'lighter';
    context.fillStyle = this.color;

    context.beginPath();
    context.arc(this.position.x, this.position.y, radius, 0, Math.PI * 2);
    context.fill();
  }

  getRemainingLifetime() {
    const elapsedLifetime = Time.now() - this.createdOn;
    return Math.max(0, this.lifetime - elapsedLifetime) / this.lifetime;
  }}


Particle.GRAVITATION = new Vector2(0, 9.81);

class Trail extends Particle {
  constructor(childFactory, position, velocity = new Vector2(), lifetime = 1, mass = 1) {
    super(position, velocity);

    this.childFactory = childFactory;
    this.children = [];
    this.lifetime = lifetime;
    this.mass = mass;

    this.isAlive = true;
  }

  update(time) {
    super.update(time);

    // Add a new child on every frame
    if (this.isAlive && this.getRemainingLifetime()) {
      this.children.push(this.childFactory(this));
    }

    // Remove particles that are dead
    this.children = this.children.filter(function (child) {
      if (child instanceof Trail) {
        return child.isAlive;
      }

      return child.getRemainingLifetime();
    });

    // Kill trail if all particles fade away
    if (!this.children.length) {
      this.isAlive = false;
    }

    // Update particles
    this.children.forEach(function (child) {
      child.update(time);
    });
  }

  render(canvas, context) {
    // Render all children
    this.children.forEach(function (child) {
      child.render(canvas, context);
    });
  }}


class Rocket extends Trail {
  constructor(childFactory, explosionFactory, position, velocity = new Vector2()) {
    super(childFactory, position, velocity);

    this.explosionFactory = explosionFactory;
    this.lifetime = 10;
  }

  update(time) {
    if (this.getRemainingLifetime() && this.velocity.y > 0) {
      this.explosionFactory(this);
      this.lifetime = 0;
    }

    super.update(time);
  }}


const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const time = new Time();
let rockets = [];

const getTrustParticleFactory = function (baseHue) {
  function getColor() {
    const hue = Math.floor(Math.random() * 15 + 30);
    return `hsl(${hue}, 100%, 75%`;
  }

  return function (parent) {
    const position = this.position.clone();
    const velocity = this.velocity.clone().multiplyScalar(-.1);
    velocity.x += (Math.random() - .5) * 8;
    const color = getColor();
    const radius = 1 + Math.random();
    const lifetime = .5 + Math.random() * .5;
    const mass = .01;

    return new Particle(position, velocity, color, radius, lifetime, mass);
  };
};

const getExplosionFactory = function (baseHue) {
  function getColor() {
    const hue = Math.floor(baseHue + Math.random() * 15) % 360;
    const lightness = Math.floor(Math.pow(Math.random(), 2) * 50 + 50);
    return `hsl(${hue}, 100%, ${lightness}%`;
  }

  function getChildFactory() {
    return function (parent) {
      const direction = Math.random() * Math.PI * 2;
      const force = 8;
      const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force);
      const color = getColor();
      const radius = 1 + Math.random();
      const lifetime = 1;
      const mass = .1;

      return new Particle(parent.position.clone(), velocity, color, radius, lifetime, mass);
    };
  }

  function getTrail(position) {
    const direction = Math.random() * Math.PI * 2;
    const force = Math.random() * 128;
    const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force);
    const lifetime = .5 + Math.random();
    const mass = .075;

    return new Trail(getChildFactory(), position, velocity, lifetime, mass);
  }

  return function (parent) {
    let trails = 32;
    while (trails--) {
      parent.children.push(getTrail(parent.position.clone()));
    }
  };
};

const addRocket = function () {
  const trustParticleFactory = getTrustParticleFactory();
  const explosionFactory = getExplosionFactory(Math.random() * 360);

  const position = new Vector2(Math.random() * canvas.width, canvas.height);
  const thrust = window.innerHeight * .75;
  const angle = Math.PI / -2 + (Math.random() - .5) * Math.PI / 8;
  const velocity = new Vector2(Math.cos(angle) * thrust, Math.sin(angle) * thrust);
  const lifetime = 3;

  rockets.push(new Rocket(trustParticleFactory, explosionFactory, position, velocity, lifetime));

  rockets = rockets.filter(function (rocket) {
    return rocket.isAlive;
  });
};

const render = function () {
  requestAnimationFrame(render);

  time.update();
  context.clearRect(0, 0, canvas.width, canvas.height);

  rockets.forEach(function (rocket) {
    rocket.update(time);
    rocket.render(canvas, context);
  });
};

const resize = function () {
  canvas.height = window.innerHeight;
  canvas.width = window.innerWidth;
};

canvas.onclick = addRocket;
document.body.appendChild(canvas);

window.onresize = resize;
resize();

setInterval(addRocket, 2000);
render();